# This file is part of GUP.
#
# GUP is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# GUP is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with GUP; if not, write to the Free Software Foundation, Inc., 51
# Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# Copyright (C) 2007 Julio Biason

'''Classes used to talk to Web Gallery 2.0'''
__revision__ = 0.1

import logging
import urllib2
import cookielib
import exceptions
import mimetypes

# Exceptions
class GalleryError(exceptions.Exception): 
    '''Generic error in the request'''
    def __str__(self):
        return GalleryError.__doc__

class ConnectionError(GalleryError):
    '''Connection error.'''
    def __str__(self):
        return ConnectionError.__doc__

class MajorVersionInvalidError(GalleryError):
    '''The protocol major version the client is using is not supported.'''
    def __str__(self):
        return MajorVersionInvalidError.__doc__

class MinorVersionInvalidError(GalleryError):
    '''The protocol minor version the client is using is not supported.'''
    def __str__(self):
        return MinorVersionInvalidError.__doc__

class ProtocolFormatInvalidError(GalleryError):
    '''The format of the protocol version string the client sent 
    in the request is invalid.'''
    def __str__(self):
        return ProtocolFormatInvalidError.__doc__

class MissingProtocolVersionError(GalleryError):
    '''The request did not contain the required protocol_version key.'''
    def __str__(self):
        return MissingProtocolVersionError.__doc__

class InvalidPasswordError(GalleryError):
    '''The password and/or username the client send in the request 
    is invalid.'''
    def __str__(self):
        return InvalidPasswordError.__doc__

class MissingLoginError(GalleryError):
    '''The client used the login command in the request but failed
    to include either the username or password (or both) in the
    request.'''
    def __str__(self):
        return MissingLoginError.__doc__

class InvalidCommandError(GalleryError):
    '''The value of the cmd key is not valid.'''
    def __str__(self):
        return InvalidCommandError.__doc__

class NoPermissionError(GalleryError):
    '''The user does not have permission to add an item to the
    gallery.'''
    def __str__(self):
        return NoPermissionError.__doc__

class NoFilenameError(GalleryError):
    '''No filename was specified.'''
    def __str__(self):
        return NoFilenameError.__doc__

class PhotoUploadError(GalleryError):
    '''The file was received, but could not be processed or
    added to the album.'''
    def __str__(self):
        return PhotoUploadError.__doc__

class NoWritePermissionError(GalleryError):
    '''No write permission to destination album.'''
    def __str__(self):
        return NoWritePermissionError.__doc__

class NoViewPermissionError(GalleryError):
    '''No view permission for this image.'''
    def __str__(self):
        return NoViewPermissionError.__doc__

class NoAlbumPermissionError(GalleryError):
    '''A new album could not be created because the user does
    not have permission to do so.'''
    def __str__(self):
        return NoAlbumPermissionError.__doc__

class AlbumCreateError(GalleryError):
    '''A new album could not be created, for a different reason
    (name conflict).'''
    def __str__(self):
        return AlbumCreateError.__doc__

class MoveAlbumError(GalleryError):
    '''The album could not be moved.'''
    def __str__(self):
        return MoveAlbumError.__doc__

class ImageRotateError(GalleryError):
    '''The image could not be rotated'''
    def __str__(self):
        return ImageRotateError.__doc__
    
def _multipart(boundary, arguments, file_info):
    '''Generates the body of a multipart data'''
    parts = []
    for key, value in arguments.iteritems():
        logging.debug('Adding "%s"...', key)
        parts.append('--%s' % boundary)
        parts.append('Content-disposition: form-data; name="%s"' % key)
        parts.append('')
        parts.append(value)

    if file_info is not None:
        content_type = mimetypes.guess_type(file_info[1])[0] or \
                'application/octet-stream'

        logging.debug('Adding file "%s" (%s) to "%s"...' % 
                (file_info[1], content_type, file_info[0]))
        parts.append('--%s' % (boundary))
        parts.append('Content-disposition: form-data; ' + \
                'name="%s"; filename="%s"' %
                (file_info[0], file_info[1]))
        parts.append('Content-Type: %s' % content_type)
        parts.append('Content-Transfer-Encoding: base64')
        parts.append('')

        image = file(file_info[1], 'rb')
        contents = image.read()
        logging.debug('Read %s, length %d', str(file_info[1]), int(len(contents)))
        image.close()

        parts.append(contents)

    parts.append('--%s--' % boundary)

    return '\r\n'.join(parts)

# Main class
class Gallery(object):
    '''Gallery interaction class'''
    def __init__(self, url, user, password):
        '''Class initiator.

        url      - Gallery installation URL
        user     - login user
        password - user passowrd'''

        self.logged   = False
        self.cookie   = None
        self.url      = url + '/main.php'
        self.user     = user
        self.password = password
        self.last_authtoken = ''

        # maps return codes to the exceptions
        self._return_codes = {
                101: MajorVersionInvalidError,
                102: MinorVersionInvalidError,
                103: ProtocolFormatInvalidError,
                104: MissingProtocolVersionError,
                201: InvalidPasswordError,
                202: MissingLoginError,
                301: InvalidCommandError,
                401: NoPermissionError,
                402: NoFilenameError,
                403: PhotoUploadError,
                404: NoWritePermissionError,
                405: NoViewPermissionError,
                501: NoAlbumPermissionError,
                502: AlbumCreateError,
                503: MoveAlbumError,
                504: ImageRotateError
                }

    def request(self, command, version, arguments, file_info = None):
        '''Send a request to the remote server. Returns a dictionary with
        the resulting variables.

        command - the command to be send
        version - command version
        arguments - dictionary with the arguments to the command
        file_info - a tuple with the field name and the filename to be added
          in the body'''
        if not self.logged and not command == 'login':
            # hate those hardcoded options
            self.login()

        logging.debug('Opening request with "%s"', self.url)
        boundary = '------GUP_Boundary'

        # those are the default fields
        data = {
                'g2_controller'            : 'remote:GalleryRemote',
                'g2_form[cmd]'             : command,
                'g2_form[protocol_version]': str(version),
                'g2_authToken'             : self.last_authtoken
                }
        data.update(arguments)

        request = urllib2.Request(self.url)
        request.add_header('User-agent', 'GUP %s' % (__revision__))
        request.add_header('Content-type', 
                'multipart/form-data; boundary=%s' % boundary)
        request.add_header('Accept', 'text/plain')
        if self.cookie is not None:
            self.cookie.add_cookie_header(request)
        else:
            cookiejar = cookielib.CookieJar()
            cookie_opener = urllib2.build_opener(
                    urllib2.HTTPCookieProcessor(cookiejar))
            urllib2.install_opener(cookie_opener)

        request.add_data(_multipart(boundary, data, file_info))

        response = urllib2.urlopen(request)

        if self.cookie is None:
            self.cookie = cookiejar

        data = response.read()
        logging.debug('== DATA ==')
        logging.debug(data)
        logging.debug('== /DATA ==')

        status = 0
        return_value = []

        for line in data.split('\n'):
            if len(line) == 0:
                continue

            if line[0] == '#':
                continue

            values = line.split('=')
            if len(values) < 2:
                continue

            if values[0] == 'status':
                status = int(values[1])
            elif values[0] == 'auth_token':
                self.last_authtoken = values[1]

            return_value.append( (values[0], values[1]) )

        return (status, return_value)

    def login(self):
        '''Login in the system'''
        arguments = {'g2_form[uname]': self.user,
                'g2_form[password]': self.password}
        logging.debug('User [%s] Password [%s]',
                self.user, self.password)

        (status, _) = self.request('login', '2.0', arguments)
        if status is None:
            raise ConnectionError

        if not status == 0:
            raise self._return_codes[status]

        self.logged = True

    def fetch_albums_prune(self):
        '''Request a list of albums'''
        (status, data) = self.request('fetch-albums-prune', '2.2', {})
        if status is None:
            raise ConnectionError

        if not status == 0:
            raise self._return_codes[status]

        last_album    = ''
        album_name    = None
        album_parent  = None
        album_title   = None

        for info in data:
            if info[0][-2:] != last_album:
                last_album = info[0][-2:]

                if album_name is not None and \
                        album_title is not None and \
                        album_parent is not None:
                    yield (int(album_name), album_title, int(album_parent))

                album_name   = None
                album_parent = None
                album_title  = None

            fields = info[0].split('.')
            if not fields[0] == 'album':
                continue

            if fields[1] == 'name':
                album_name = info[1]
            elif fields[1] == 'parent':
                album_parent = info[1]
            elif fields[1] == 'title':
                album_title = info[1]

        if album_name is not None and \
                album_title is not None and \
                album_parent is not None:
            yield (int(album_name), album_title, int(album_parent))

    def new_album(self, album_parent, album_name, album_title):
        '''Create a new album'''
        (status, data) = self.request('new-album', '2.1', {
            'g2_form[set_albumName]': album_parent,
            'g2_form[newAlbumName]': album_name,
            'g2_form[newAlbumTitle]': album_title,
            'g2_form[newAlbumDesc]': ''})

        if status is None:
            raise ConnectionError

        if not status == 0:
            raise self._return_codes[status]

        for info in data:
            if info[0] == 'album_name':
                logging.debug('Created album "%s"', info[1])
                return int(info[1])

        return None

    def add_item(self, album, filename):
        '''Add an item to an album'''
        (status, _) = self.request('add-item', '2.0', {
            'g2_form[set_albumName]': album,
            'g2_userfile_name': filename
            }, 
            ('g2_userfile', filename)
            )

        if status is None:
            raise ConnectionError

        if not status == 0:
            raise self._return_codes[status]
